<?php

namespace app\command\workerman\process\httpServer;

use think\facade\App;
use Workerman\Timer;

class Monitor
{
	private bool $stop = false;

	private int|null $timer = null;

	private string $lockFile = '';


	private string|null $memoryLimit = null;

	private array $paths = [];

	private array $extensions = [];

	/**
	 * Pause monitor
	 * @return void
	 */
	private function pause(): void
	{
		file_put_contents($this->lockFile, time());
	}

	/**
	 * Resume monitor
	 * @return void
	 */
	private function resume(): void
	{
		clearstatcache();
		if (is_file($this->lockFile)) {
			unlink($this->lockFile);
		}
	}

	/**
	 * Whether monitor is paused
	 * @return bool
	 */
	private function isPaused(): bool
	{
		clearstatcache();
		return file_exists($this->lockFile);
	}

	public function __construct(array $config = [])
	{
		$this->resume();

		if (isset($config['monitorDir'])) $this->paths = $config['monitorDir'];

		if (isset($config['monitorExtensions'])) $this->extensions = $config['monitorExtensions'];

		if (isset($config['memoryLimit'])) $this->memoryLimit = $config['memoryLimit'];

		if (isset($config['lockFile'])) {
			$this->lockFile = $config['lockFile'];
		} else {
			$this->lockFile = App::getRuntimePath() . 'workerman/monitor.lock';
		}
		if (empty($this->lockFile)) {
			throw new \Exception('not set $lockFile');
		}
	}

	public function onWorkerStart()
	{
		global $argv;
		$isDaemon = in_array('-d', $argv);

		// 开发者模式 和 wk调试下 热更新
		if (App::isDebug() && !$isDaemon) {
			$this->checkAllFilesChange();
		}

		// 超出最大进程内存限制重启进程
		$memoryLimit = $this->getMemoryLimit($this->memoryLimit ?? null);
		if ($memoryLimit) {
			Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
		}
	}

	private function checkAllFilesChange(): void
	{
		if ($this->isPaused()) {
			return;
		}

		$this->timer = Timer::add(2, function () {

			if ($this->stop) {
				Timer::del($this->timer);
				return;
			}

			foreach ($this->paths as $path) {

				static $lastMtime, $tooManyFilesCheck;

				if (!$lastMtime) {
					$lastMtime = time();
				}

				clearstatcache();


				if (!is_dir($path)) {
					if (!is_file($path)) {
						break;
					}
					$iterator = [new \SplFileInfo($path)];
				} else {
					$dirIterator = new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS);

					$iterator = new \RecursiveIteratorIterator($dirIterator);
				}

				$count = 0;

				foreach ($iterator as $file) {
					$count++;

					if (
						in_array($file->getExtension(), $this->extensions, true) &&
						$lastMtime < $file->getMTime()
					) {

						if (!posix_kill(posix_getppid(), SIGUSR1)) {
							dump('[monitor] require root user');
							$this->stop = true;
							break;
						} else {
							dump('[monitor:update] ' . $file);
						}

						$lastMtime = $file->getMTime();
						break;
					}
				}

				if (!$tooManyFilesCheck && $count > 1000) {
					dump("[monitor]: There are too many files ($count files) in $path which makes file monitoring very slow");
					$tooManyFilesCheck = 1;
				}

			}

		});
	}

	/**
	 * @param $memoryLimit
	 * @return void
	 */
	public function checkMemory($memoryLimit): void
	{
		if ($this->isPaused() || $memoryLimit <= 0) {
			return;
		}
		$ppid         = posix_getppid();
		$childrenFile = "/proc/$ppid/task/$ppid/children";
		if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
			return;
		}
		foreach (explode(' ', $children) as $pid) {
			$pid        = (int)$pid;
			$statusFile = "/proc/$pid/status";
			if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
				continue;
			}
			$mem = 0;
			if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
				$mem = $match[1];
			}
			$mem = (int)($mem / 1024);
			if ($mem >= $memoryLimit) {
				posix_kill($pid, SIGINT);
			}
		}
	}

	private function getMemoryLimit($memoryLimit): float|int
	{
		if ($memoryLimit === 0) {
			return 0;
		}
		$usePhpIni = false;
		if (!$memoryLimit) {
			$memoryLimit = ini_get('memory_limit');
			$usePhpIni   = true;
		}

		if ($memoryLimit == -1) {
			return 0;
		}

		$unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
		if ($unit === 'g') {
			$memoryLimit = 1024 * (int)$memoryLimit;
		} else if ($unit === 'm') {
			$memoryLimit = (int)$memoryLimit;
		} else if ($unit === 'k') {
			$memoryLimit = ((int)$memoryLimit / 1024);
		} else {
			$memoryLimit = ((int)$memoryLimit / (1024 * 1024));
		}
		if ($memoryLimit < 30) {
			$memoryLimit = 30;
		}
		if ($usePhpIni) {
			$memoryLimit = (int)(0.8 * $memoryLimit);
		}
		return $memoryLimit;
	}
}